package com.zhentao.service.impl;

import com.zhentao.config.RecommendProps;
import com.zhentao.mapper.RecommendMapper;
import com.zhentao.service.RecommendService;
import com.zhentao.vo.RecommendUserVO;
import com.zhentao.dto.UserSearchQuery;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Resource;
import java.util.List;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.core.StringRedisTemplate;

@Service
public class RecommendServiceImpl implements RecommendService {

    private static final Logger log = LoggerFactory.getLogger(RecommendServiceImpl.class);

    @Resource
    private RecommendMapper recommendMapper;

    @Resource
    private RecommendProps recommendProps;

    private final ObjectMapper objectMapper = new ObjectMapper();
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public List<RecommendUserVO> getRecommendedUsers(Integer userId, Integer oppoOnly, Integer limit) {
        if (userId == null) {
            throw new IllegalArgumentException("userId cannot be null");
        }
        if (oppoOnly == null) {
            oppoOnly = 1; // default opposite gender
        }
        if (limit == null || limit <= 0) {
            limit = 50;
        }
        // 读取或生成候选池缓存
        int poolSize = Math.min(Math.max(limit * 5, 100), 500);
        String poolKey = "rec:pool:" + userId + ":" + oppoOnly + ":" + poolSize;
        List<RecommendUserVO> pool = null;
        if (recommendProps.isCacheEnabled()) {
            try {
                String cached = stringRedisTemplate.opsForValue().get(poolKey);
                if (cached != null && !cached.isEmpty()) {
                    pool = objectMapper.readValue(cached, new TypeReference<List<RecommendUserVO>>(){});
                }
            } catch (Exception ignore) {}
        }
        if (pool == null) {
            try {
                pool = recommendMapper.selectRecommendedUsers(userId, oppoOnly, poolSize);
            } catch (Exception ex) {
                log.error("selectRecommendedUsers failed, fallback to empty list (possibly DB not available)", ex);
                return java.util.Collections.emptyList();
            }
            if (recommendProps.isCacheEnabled() && pool != null && !pool.isEmpty()) {
                try {
                    String json = objectMapper.writeValueAsString(pool);
                    stringRedisTemplate.opsForValue().set(poolKey, json, recommendProps.getCacheTtlSeconds(), TimeUnit.SECONDS);
                } catch (Exception ignore) {}
            }
        }

        // 过滤用户的"不喜欢"集合，避免再次出现
        try {
            java.util.Set<String> disliked = stringRedisTemplate.opsForSet().members("rec:dislike:user:" + userId);
            if (disliked != null && !disliked.isEmpty()) {
                pool.removeIf(u -> u.getUserId() != null && disliked.contains(String.valueOf(u.getUserId())));
            }
        } catch (Exception ignore) {}

        // 迭代式 MMR：每次从候选中选择 reRank 分最高者加入结果集
        double lambda = recommendProps.getMmrLambda();
        List<RecommendUserVO> result = new ArrayList<>();
        Set<String> seenBuckets = new HashSet<>();

        // 先按原始分降序，作为MMR的初始候选顺序（空值按0处理）
        pool.sort(Comparator.<RecommendUserVO>comparingDouble((RecommendUserVO u) -> safeScore(u.getCompatibilityScore())).reversed());

        while (result.size() < limit && !pool.isEmpty()) {
            double bestScore = Double.NEGATIVE_INFINITY;
            int bestIdx = -1;

            for (int i = 0; i < Math.min(pool.size(), 200); i++) { // 每轮评估前200个
                RecommendUserVO cand = pool.get(i);

                // 桶去重：过多重复的城/校/职，不再考虑
                String bucket = (cand.getCityId() == null ? "-" : cand.getCityId()) + "|"
                        + (cand.getSchoolName() == null ? "-" : cand.getSchoolName()) + "|"
                        + (cand.getJobTitle() == null ? "-" : cand.getJobTitle());
                if (seenBuckets.contains(bucket)) {
                    continue;
                }

                double maxSim = 0.0;
                for (RecommendUserVO chosen : result) {
                    double sim = 0.0;
                    // 余弦相似度扩展：兴趣 + 职位 + 院校 三路向量加权
                    double hobbyCos = cosineSimilarity(tokenizeHobby(cand.getHobby()), tokenizeHobby(chosen.getHobby()));
                    double jobCos = cosineSimilarity(tokenizeText(cand.getJobTitle()), tokenizeText(chosen.getJobTitle()));
                    double schoolCos = cosineSimilarity(tokenizeText(cand.getSchoolName()), tokenizeText(chosen.getSchoolName()));
                    double cosCombined = 0.5 * hobbyCos + 0.3 * jobCos + 0.2 * schoolCos; // 权重可后续配置化
                    sim += cosCombined;
                    if (sim > maxSim) maxSim = sim;
                }

                double reRankScore = safeScore(cand.getCompatibilityScore()) - lambda * maxSim * 100.0;
                if (reRankScore > bestScore) {
                    bestScore = reRankScore;
                    bestIdx = i;
                }
            }

            if (bestIdx >= 0) {
                RecommendUserVO chosen = pool.remove(bestIdx);
                String bucket = (chosen.getCityId() == null ? "-" : chosen.getCityId()) + "|"
                        + (chosen.getSchoolName() == null ? "-" : chosen.getSchoolName()) + "|"
                        + (chosen.getJobTitle() == null ? "-" : chosen.getJobTitle());
                seenBuckets.add(bucket);
                result.add(chosen);
            } else {
                break;
            }
        }

        // 写入最终 TopN 缓存
        if (recommendProps.isCacheEnabled() && !result.isEmpty()) {
            try {
                String topKey = "rec:top:" + userId + ":" + oppoOnly + ":" + limit;
                String json = objectMapper.writeValueAsString(result);
                stringRedisTemplate.opsForValue().set(topKey, json, recommendProps.getCacheTtlSeconds(), TimeUnit.SECONDS);
            } catch (Exception ignore) {}
        }

        return result;
    }

    @Override
    public List<RecommendUserVO> searchByRules(UserSearchQuery q) {
        if (q == null || q.getUserId() == null) {
            throw new IllegalArgumentException("userId cannot be null");
        }
        int limit = (q.getLimit() == null || q.getLimit() <= 0) ? 20 : q.getLimit();
        int offset = (q.getOffset() == null || q.getOffset() < 0) ? 0 : q.getOffset();

        // 计算出生日期范围（年龄 -> 出生日期）
        String birthMin = null, birthMax = null; // YYYY-MM-DD
        java.time.LocalDate today = java.time.LocalDate.now();
        if (q.getAgeMax() != null) {
            birthMin = today.minusYears(q.getAgeMax()).toString();
        }
        if (q.getAgeMin() != null) {
            birthMax = today.minusYears(q.getAgeMin()).toString();
        }

        // 兴趣标签转 JSON 数组字符串
        String hobbyJson = null;
        if (q.getHobbyTags() != null && !q.getHobbyTags().isEmpty()) {
            try { hobbyJson = objectMapper.writeValueAsString(q.getHobbyTags()); } catch (Exception ignore) {}
        }

        // 读取不喜欢集合，作为排除列表
        java.util.List<Integer> excludeIds = new java.util.ArrayList<>();
        try {
            java.util.Set<String> disliked = stringRedisTemplate.opsForSet().members("rec:dislike:user:" + q.getUserId());
            if (disliked != null) {
                for (String s : disliked) { try { excludeIds.add(Integer.parseInt(s)); } catch (Exception ignore) {} }
            }
        } catch (Exception ignore) {}

        // 召回
        List<RecommendUserVO> pool;
        try {
            pool = recommendMapper.selectByRules(q, birthMin, birthMax, hobbyJson, excludeIds, Math.min(limit * 5, 200), offset);
        } catch (Exception ex) {
            log.error("selectByRules failed, fallback to empty list (possibly DB not available)", ex);
            return java.util.Collections.emptyList();
        }

        // 重排（沿用 MMR）
        double lambda = recommendProps.getMmrLambda();
        java.util.List<RecommendUserVO> result = new java.util.ArrayList<>();
        java.util.Set<String> seenBuckets = new java.util.HashSet<>();
        pool.sort(java.util.Comparator.<RecommendUserVO>comparingDouble((RecommendUserVO u) -> safeScore(u.getCompatibilityScore())).reversed());
        while (result.size() < limit && !pool.isEmpty()) {
            double bestScore = Double.NEGATIVE_INFINITY; int bestIdx = -1;
            for (int i = 0; i < Math.min(pool.size(), 200); i++) {
                RecommendUserVO cand = pool.get(i);
                String bucket = (cand.getCityId() == null ? "-" : cand.getCityId()) + "|"
                        + (cand.getSchoolName() == null ? "-" : cand.getSchoolName()) + "|"
                        + (cand.getJobTitle() == null ? "-" : cand.getJobTitle());
                if (seenBuckets.contains(bucket)) continue;
                double hobbyCos = cosineSimilarity(tokenizeHobby(cand.getHobby()), new String[0]);
                double jobCos = cosineSimilarity(tokenizeText(cand.getJobTitle()), new String[0]);
                double schoolCos = cosineSimilarity(tokenizeText(cand.getSchoolName()), new String[0]);
                double maxSim = 0.5 * hobbyCos + 0.3 * jobCos + 0.2 * schoolCos;
                double reRankScore = safeScore(cand.getCompatibilityScore()) - lambda * maxSim * 100.0;
                if (reRankScore > bestScore) { bestScore = reRankScore; bestIdx = i; }
            }
            if (bestIdx >= 0) {
                RecommendUserVO chosen = pool.remove(bestIdx);
                String bucket = (chosen.getCityId() == null ? "-" : chosen.getCityId()) + "|"
                        + (chosen.getSchoolName() == null ? "-" : chosen.getSchoolName()) + "|"
                        + (chosen.getJobTitle() == null ? "-" : chosen.getJobTitle());
                seenBuckets.add(bucket); result.add(chosen);
            } else break;
        }
        return result;
    }

    @Override
    public List<com.zhentao.pojo.City> getAllCities() {
        return recommendMapper.selectAllCities();
    }

    @Override
    public List<com.zhentao.pojo.Area> getAreasByCity(Integer cityId) {
        if (cityId == null) return java.util.Collections.emptyList();
        return recommendMapper.selectAreasByCity(cityId);
    }

    private boolean safeEq(String a, String b) {
        if (a == null || b == null) return false;
        return a.equals(b);
    }

    private boolean jobSimilar(String a, String b) {
        if (a == null || b == null) return false;
        String as = a.toLowerCase();
        String bs = b.toLowerCase();
        return as.equals(bs) || as.contains(bs) || bs.contains(as);
    }

    private boolean hobbyOverlap(String a, String b) {
        if (a == null || b == null) return false;
        String as = a.toLowerCase();
        String bs = b.toLowerCase();
        String[] tokens = as.replace('[',' ').replace(']',' ').replace('"',' ').split("[, ]+");
        for (String t : tokens) {
            if (t == null || t.trim().isEmpty()) continue;
            if (bs.contains(t.trim())) return true;
        }
        return false;
    }

    private String[] tokenizeHobby(String hobbyJson) {
        if (hobbyJson == null) return new String[0];
        String merged = hobbyJson.toLowerCase().replace('[',' ').replace(']',' ').replace('"',' ');
        return merged.split("[^a-z0-9\\u4e00-\\u9fa5]+");
    }

    private String[] tokenizeText(String text) {
        if (text == null) return new String[0];
        return text.toLowerCase().split("[^a-z0-9\\u4e00-\\u9fa5]+");
    }

    private double cosineSimilarity(String[] a, String[] b) {
        if (a == null || b == null || a.length == 0 || b.length == 0) return 0.0;
        java.util.Map<String, int[]> map = new java.util.HashMap<>();
        for (String t : a) {
            if (t == null || t.isEmpty()) continue;
            map.computeIfAbsent(t, k -> new int[2])[0]++;
        }
        for (String t : b) {
            if (t == null || t.isEmpty()) continue;
            map.computeIfAbsent(t, k -> new int[2])[1]++;
        }
        double dot = 0, na = 0, nb = 0;
        for (int[] v : map.values()) {
            dot += v[0] * v[1];
            na += v[0] * v[0];
            nb += v[1] * v[1];
        }
        if (na == 0 || nb == 0) return 0.0;
        return dot / (Math.sqrt(na) * Math.sqrt(nb));
    }

    private double safeScore(Number n) {
        if (n == null) return 0.0;
        try { return n.doubleValue(); } catch (Exception ignore) { return 0.0; }
    }
}


